﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

// TgaImage.cs -- Load TGA images.
//
// The source code in this file is based on the TargaImage class developed by 
// David Polomis and licensed under the Code Project Open License (CPOL) 1.02.
// The original code along with documentation can be found at
// http://www.codeproject.com/Articles/31702/NET-Targa-Image-Reader

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.ComponentModel;

// Bugs in TargaImage:
// 1 - doesn't work with flipped horizontal order
// 2 - software version is missing
// 3 - doesn't support dev area

namespace Util.Media
{
    #region Structures

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaFileHeader
    {
        [DefaultValue((byte)0)]
        public byte ImageIdLength { get; set; }

        [DefaultValue(TgaPaletteType.None)]
        public TgaPaletteType PaletteType { get; set; }

        [DefaultValue(TgaImageType.None)]
        public TgaImageType ImageType { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort PaletteStartIndex { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort PaletteEntryCount { get; set; }

        /// <summary>
        /// Specifies the number of bits per palette entry.
        /// </summary>
        [DefaultValue((byte)0)]
        public byte PaletteColorDepth { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort ImageOriginX { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort ImageOriginY { get; set; }

        public ushort ImageWidth { get; set; }

        public ushort ImageHeight { get; set; }

        /// <summary>
        /// Number of bits per pixel including the alpha channel. Common 
        /// values are 8, 16, 24 and 32 but other values can also be used.
        /// </summary>
        public byte PixelDepth { get; set; }

        [DefaultValue((byte)0)]
        public byte ImageDescriptor { get; set; }

        /// <summary>
        /// Gets or sets the id of the image.
        /// </summary>
        /// <remarks>
        /// Technically the Image ID field is not part of the TGA header as 
        /// described in TGA 2.0 spec, but since the Image ID field always
        /// follows the File Header, it is convenient to include it in the
        /// TgaFileHeader structure. 
        /// 
        /// In addition, according to TGA 2.0 spec, the Image ID field is not
        /// necessarily a string. Here we treat it like a string to simplify
        /// display.
        /// </remarks>
        public string ImageId { get; set; }

        public override string ToString()
        {
            return string.Format("{0} x {1}, {3}, {2} bpp",
                this.ImageWidth, this.ImageHeight, this.PixelDepth, this.ColorFormat);
        }

        /// <summary>
        /// Gets the number of bits per pixel used by the alpha channel.
        /// </summary>
        [DefaultValue((byte)0)]
        public byte AlphaDepth
        {
            get { return (byte)(ImageDescriptor & 0x0F); }
            set
            {
                if (value >= 16)
                    throw new ArgumentException("AlphaDepth must be between 0 and 15.");
                ImageDescriptor = (byte)(ImageDescriptor & 0xF0 | value);
            }
        }

        [DefaultValue(TgaHorizontalDirection.LeftToRight)]
        public TgaHorizontalDirection HorizontalDirection
        {
            get { return (TgaHorizontalDirection)((ImageDescriptor >> 4) & 0x01); }
            set
            {
                if (value != TgaHorizontalDirection.LeftToRight &&
                    value != TgaHorizontalDirection.RightToLeft)
                    throw new ArgumentException("Invalid value for HorizontalDirection.");
                ImageDescriptor = (byte)(ImageDescriptor & 0xEF | ((int)value << 4));
            }
        }

        [DefaultValue(TgaVerticalDirection.BottomToTop)]
        public TgaVerticalDirection VerticalDirection
        {
            get { return (TgaVerticalDirection)((ImageDescriptor >> 5) & 0x01); }
            set
            {
                if (value != TgaVerticalDirection.BottomToTop &&
                    value != TgaVerticalDirection.TopToBottom)
                    throw new ArgumentException("Invalid value for VerticalDirection.");
                ImageDescriptor = (byte)(ImageDescriptor & 0xDF | ((int)value << 5));
            }
        }

        public TgaColorFormat ColorFormat
        {
            get { return (TgaColorFormat)((int)ImageType & 3); }
        }

        public bool IsCompressed
        {
            get
            {
                return ImageType == TgaImageType.IndexedCompressed ||
                       ImageType == TgaImageType.GrayScaleCompressed ||
                       ImageType == TgaImageType.TrueColorCompressed;
            }
        }

        /// <summary>
        /// Gets the Bitmap-compatible pixel format of this image, or
        /// <code>PixelFormat.Undefined</code> if the format of this image
        /// is not supported.
        /// </summary>
        public PixelFormat GetPixelFormat(TgaAlphaType alphaType)
        {
            int bpp = this.PixelDepth;
            if (ColorFormat == TgaColorFormat.Indexed)
            {
                if (bpp == 8)
                    return PixelFormat.Format8bppIndexed;
                return PixelFormat.Undefined;
            }

            if (ColorFormat == TgaColorFormat.GrayScale)
            {
                if (bpp == 8)
                    return PixelFormat.Format8bppIndexed;
                if (bpp == 16)
                    return PixelFormat.Format16bppGrayScale;
                return PixelFormat.Undefined;
            }

            if (ColorFormat == TgaColorFormat.TrueColor)
            {
                if (bpp == 15)
                {
                    return PixelFormat.Format16bppRgb555;
                }
                if (bpp == 16)
                {
                    if (alphaType == TgaAlphaType.Present && AlphaDepth == 1)
                        return PixelFormat.Format16bppArgb1555;
                    else
                        return PixelFormat.Format16bppRgb555;
                }
                if (bpp == 24)
                {
                    return PixelFormat.Format24bppRgb;
                }
                if (bpp == 32)
                {
                    if (alphaType == TgaAlphaType.Present && AlphaDepth == 8)
                        return PixelFormat.Format32bppArgb;
                    if (alphaType == TgaAlphaType.PreMultiplied && AlphaDepth == 8)
                        return PixelFormat.Format32bppPArgb;
                    return PixelFormat.Format32bppRgb;
                }
                return PixelFormat.Undefined;
            }

            return PixelFormat.Undefined;
        }
    }

    /// <summary>
    /// Defines the type of color map included in the image file. 
    /// </summary>
    public enum TgaPaletteType : byte
    {
        /// <summary>
        /// No color map is included in the file.
        /// </summary>
        None = 0,

        /// <summary>
        /// A color map is included in the file.
        /// </summary>
        Present = 1,
    }

    public enum TgaColorFormat
    {
        None = 0,
        Indexed = 1,
        TrueColor = 2,
        GrayScale = 3,
    }

    /// <summary>
    /// Defines the pixel and encoding format of the image.
    /// </summary>
    public enum TgaImageType : byte
    {
        /// <summary>
        /// No image data is present in the file.
        /// </summary>
        None = 0,

        /// <summary>
        /// Image is an uncompressed, indexed color-mapped image.
        /// </summary>
        IndexedUncompressed = 1,

        /// <summary>
        /// Image is an uncompressed, RGB image.
        /// </summary>
        TrueColorUncompressed = 2,

        /// <summary>
        /// Image is an uncompressed, Greyscale image.
        /// </summary>
        GrayScaleUncompressed = 3,

        /// <summary>
        /// Image is a compressed, indexed color-mapped image.
        /// </summary>
        IndexedCompressed = 9,

        /// <summary>
        /// Image is a compressed, RGB image.
        /// </summary>
        TrueColorCompressed = 10,

        /// <summary>
        /// Image is a compressed, Greyscale image.
        /// </summary>
        GrayScaleCompressed = 11,
    }

    public enum TgaHorizontalDirection : byte
    {
        LeftToRight = 0,
        RightToLeft = 1,
    }

    public enum TgaVerticalDirection : byte
    {
        BottomToTop = 0,
        TopToBottom = 1,
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaFileFooter
    {
        public const int StructSize = 26;

        [DefaultValue(typeof(uint), "0")]
        public uint ExtensionAreaOffset { get; set; }

        [DefaultValue(typeof(uint), "0")]
        public uint DeveloperAreaOffset { get; set; }

        [DefaultValue(TgaImage.TgaFileSignature)]
        public string Signature { get; set; }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaDeveloperArea
    {
        public ushort EntryCount;
        public TgaDeveloperEntry[] Entries;
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaDeveloperEntry
    {
        public ushort Tag { get; set; }
        public uint Offset;
        public uint Size;
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaExtensionArea
    {
        [DefaultValue(typeof(ushort), "495")]
        public ushort ExtensionSize { get; set; }

        public string AuthorName { get; set; }

        public string[] AuthorComments { get; set; }

        public DateTime Timestamp { get; set; }

        public string JobName { get; set; }

        [DefaultValue(typeof(TimeSpan), "00:00:00")]
        public TimeSpan JobDuration { get; set; }

        public string SoftwareName { get; set; }

        public TgaVersion SoftwareVersion { get; set; }

        [DefaultValue(typeof(Color), "0, 0, 0, 0")]
        public Color BackgroundColor { get; set; }

        public TgaRatio PixelAspectRatio { get; set; }

        public TgaRatio GammaValue { get; set; }

        [DefaultValue(typeof(uint), "0")]
        public uint ColorCorrectionTableOffset { get; set; }

        [DefaultValue(typeof(uint), "0")]
        public uint ThumbnailOffset { get; set; }

        [DefaultValue(typeof(uint), "0")]
        public uint ScanLineTableOffset { get; set; }

        [DefaultValue(TgaAlphaType.None)]
        public TgaAlphaType AlphaType { get; set; }

        public uint[] ScanLineTable { get; set; }

        public TgaColorCorrectionEntry[] ColorCorrectionTable { get; set; }

        public TgaThumbnail Thumbnail { get; set; }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public struct TgaVersion
    {
        public ushort Number { get; set; }

        public byte Letter { get; set; }

        public string Value
        {
            get
            {
                return (Number / 100).ToString() + "." +
                       (Number % 100).ToString("00") +
                       (char)Letter;
            }
        }

        public override string ToString()
        {
            return this.Value;
        }
    }

    public struct TgaRatio
    {
        public ushort Numerator { get; set; }

        public ushort Denominator { get; set; }

        public override string ToString()
        {
            return (Denominator == 0) ? "" :
                Numerator.ToString() + "/" + Denominator.ToString();
        }
    }

    public enum TgaAlphaType : byte
    {
        /// <summary>
        /// No Alpha data is included in this file. Field
        /// Header.ImageSpec.AlphaDepth should also be set to zero.
        /// </summary>
        None = 0,

        /// <summary>
        /// The data in the Alpha field is undefined and can be ignored.
        /// </summary>
        Ignored = 1,

        /// <summary>
        /// The data in the Alpha field is undefined but should be retained.
        /// </summary>
        Retained = 2,

        /// <summary>
        /// Useful Alpha channel data is present.
        /// </summary>
        Present = 3,

        /// <summary>
        /// The pixel color intensities are pre-multiplied with Alpha values.
        /// </summary>
        PreMultiplied = 4,
    }

    public struct TgaColorCorrectionEntry
    {
        public ushort Blue;
        public ushort Green;
        public ushort Red;
        public ushort Alpha;
    }

    #endregion

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class TgaThumbnail
    {
        public byte Width { get; set; }
        public byte Height { get; set; }
        public byte[][] PixelData { get; set; }
    }

    /// <summary>
    /// Contains all the information in a TGA image file.
    /// </summary>
    public class TgaImage : IDisposable
    {
        // (1) TGA File Header,
        // (2) Image/ColorMap Data, 
        // (3) Developer Area,
        // (4) Extension Area
        // (5) TGA File Footer.

        public const string TgaFileSignature = "TRUEVISION-XFILE.";

        /// <summary>
        /// Gets the header of this image.
        /// </summary>
        public TgaFileHeader Header { get; private set; }

        /// <summary>
        /// Gets the palette of this image, or <code>null</code> if this
        /// image does not contain a palette.
        /// </summary>
        public Color[] Palette { get; private set; }

        /// <summary>
        /// Gets the developer area of this image, or <code>null</code> if
        /// this image does not contain a developer area.
        /// </summary>
        public TgaDeveloperArea DeveloperArea { get; private set; }

        /// <summary>
        /// Gets the extension area of this image, or <code>null</code> if
        /// this image does not contain an extension area.
        /// </summary>
        public TgaExtensionArea ExtensionArea { get; private set; }

        /// <summary>
        /// Gets the footer of this image, or <code>null</code> if this image
        /// does not contain a footer. 
        /// </summary>
        /// <remarks>
        /// An image that contains a footer is assumed to conform to TGA 2.0;
        /// an image that does not contain a footer is assumed to be in the 
        /// TGA 1.0 format.
        /// </remarks>
        public TgaFileFooter Footer { get; private set; }

        /// <summary>
        /// Gets the bitmap of this image.
        /// </summary>
        public Bitmap Image { get; private set; }

        /// <summary>
        /// Gets the thumbnail of this image, or <code>null</code> if this
        /// image does not contain a thumbnail.
        /// </summary>
        public Bitmap Thumbnail { get; private set; }

        /// <summary>
        /// Creates a new instance of the TargaImage object.
        /// </summary>
        public TgaImage() { }

        /// <summary>
        /// Loads a TGA image from a stream.
        /// </summary>
        public TgaImage(Stream stream)
        {
            using (TgaBinaryReader reader = new TgaBinaryReader(stream))
            {
                // Read file header.
                this.Header = reader.ReadFileHeader();
                if (Header.GetPixelFormat(TgaAlphaType.Present) == PixelFormat.Undefined)
                    throw new InvalidDataException("Unsupported pixel format.");

                // Read color map (palette) if present. Even if the image's
                // pixel format is not indexed, we should read this in order
                // to skip the color map section.
                if (Header.PaletteType == TgaPaletteType.Present)
                {
                    this.Palette = reader.ReadPalette(
                        Header.PaletteColorDepth,
                        Header.PaletteEntryCount);
                }

                // Read main image data.
                byte[][] mainImageData = reader.ReadPixelData(
                    Header.IsCompressed,
                    Header.PixelDepth,
                    Header.ImageWidth,
                    Header.ImageHeight);

                // Up-till now, we are working in TGA 1.0 space and never
                // needed to seek the file pointer. However, for TGA 2.0,
                // we need to test the presence of the File Footer, which
                // requires seeking to the end of the stream. We also need
                // to compare the signature: if the signature matches, the
                // stream is assumed to be TGA 2.0; otherwise, it's assumed
                // to be TGA 1.0.
                if (stream.CanSeek &&
                    stream.Length - stream.Position >= TgaFileFooter.StructSize)
                {
                    stream.Seek(-TgaFileFooter.StructSize, SeekOrigin.End);
                    this.Footer = reader.ReadFileFooter();
                    if (Footer.Signature != TgaFileSignature)
                        Footer = null;
                }

                // Load developer area if present.
                if (Footer != null && Footer.DeveloperAreaOffset > 0)
                {
                    stream.Seek(Footer.DeveloperAreaOffset, SeekOrigin.Begin);
                    this.DeveloperArea = reader.ReadDeveloperArea();
                }

                // Load extension area if present.
                if (Footer != null && Footer.ExtensionAreaOffset > 0)
                {
                    stream.Seek(Footer.ExtensionAreaOffset, SeekOrigin.Begin);
                    this.ExtensionArea = reader.ReadExtensionArea();
                    // If ExtensionArea.ExtensionSize is larger that 495, we 
                    // are probably dealing with some future version of TGA. 
                    // Keep this in mind in case we want to reproduce the file.
                }

                // Load Scan Line Table if present.
                if (ExtensionArea != null && ExtensionArea.ScanLineTableOffset > 0)
                {
                    stream.Seek(ExtensionArea.ScanLineTableOffset, SeekOrigin.Begin);
                    ExtensionArea.ScanLineTable = reader.ReadScanlineTable(Header.ImageHeight);
                }

                // Load Color Correction Table if present.
                if (ExtensionArea != null && ExtensionArea.ColorCorrectionTableOffset > 0)
                {
                    stream.Seek(ExtensionArea.ColorCorrectionTableOffset, SeekOrigin.Begin);
                    ExtensionArea.ColorCorrectionTable = reader.ReadColorCorrectionTable();
                }

                // Load thumbnail if present.
                if (ExtensionArea != null && ExtensionArea.ThumbnailOffset > 0)
                {
                    stream.Seek(ExtensionArea.ThumbnailOffset, SeekOrigin.Begin);
                    ExtensionArea.Thumbnail = reader.ReadThumbnail(Header.PixelDepth);
                }

                // Now we have all the data available. In particular, we have
                // ExtensionArea.AlphaType, which affects the pixel format of
                // the image.
                TgaAlphaType alphaType = (ExtensionArea == null) ?
                    TgaAlphaType.Present : ExtensionArea.AlphaType;

                // Create the main image.
                this.Image = TgaBitmapHelper.CreateBitmap(
                    Header.ImageWidth,
                    Header.ImageHeight,
                    Header.GetPixelFormat(alphaType),
                    mainImageData,
                    Header.HorizontalDirection,
                    Header.VerticalDirection);

                // Fill the palette of the main image.
                ColorPalette bitmapPalette = null;
                if (this.Header.ColorFormat == TgaColorFormat.GrayScale)
                {
                    bitmapPalette = TgaBitmapHelper.FillGrayScalePalette(this.Image);
                }
                if (Header.ColorFormat == TgaColorFormat.Indexed &&
                    this.Palette != null)
                {
                    bitmapPalette = TgaBitmapHelper.FillIndexedPalette(
                        this.Image,
                        this.Palette,
                        Header.PaletteStartIndex,
                        alphaType != TgaAlphaType.Present);
                }

                // Create the thumbnail image if available.
                if (ExtensionArea != null && ExtensionArea.Thumbnail != null)
                {
                    this.Thumbnail = TgaBitmapHelper.CreateBitmap(
                        ExtensionArea.Thumbnail.Width,
                        ExtensionArea.Thumbnail.Height,
                        Header.GetPixelFormat(alphaType),
                        ExtensionArea.Thumbnail.PixelData,
                        Header.HorizontalDirection,
                        Header.VerticalDirection);
                    if (bitmapPalette != null)
                        this.Thumbnail.Palette = bitmapPalette;
                }
            }
        }

        /// <summary>
        /// Disposes all resources used by this object. After disposal, the
        /// <code>Image</code> and <code>Thumbnail</code> properties are no
        /// longer valid.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposing)
                return;

            if (this.Image != null)
            {
                this.Image.Dispose();
                this.Image = null;
            }
            if (this.Thumbnail != null)
            {
                this.Thumbnail.Dispose();
                this.Thumbnail = null;
            }
        }
    }

    internal static class TgaBitmapHelper
    {
        public static Bitmap CreateBitmap(
            int width, int height,
            PixelFormat format, byte[][] scanlines,
            TgaHorizontalDirection horizontalDirection,
            TgaVerticalDirection verticalDirection)
        {
            // Create a Bitmap with the specified size and format.
            Bitmap bmp = new Bitmap(width, height, format);

            // Lock the bitmap and copy the scanline data.
            BitmapData bmpData = bmp.LockBits(
                new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, format);
            IntPtr ptr = bmpData.Scan0;
            int stride = bmpData.Stride;
            if (verticalDirection == TgaVerticalDirection.BottomToTop)
            {
                ptr += (height - 1) * stride;
                stride = -stride;
            }
            for (int y = 0; y < height; y++)
            {
                Marshal.Copy(scanlines[y], 0, ptr, scanlines[y].Length);
                ptr += stride;
            }
            bmp.UnlockBits(bmpData);

            // Flip the bitmap if the pixels in each scanline are reversed.
            if (horizontalDirection == TgaHorizontalDirection.RightToLeft)
                bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);

            return bmp;
        }

        public static ColorPalette FillGrayScalePalette(Bitmap bmp)
        {
            // Get a COPY of the color palette used in the bitmap.
            ColorPalette palette = bmp.Palette;
            if (palette.Entries.Length != 256)
                throw new InvalidOperationException("A grayscale palette must have 256 colors.");

            for (int i = 0; i < 256; i++)
                palette.Entries[i] = Color.FromArgb(i, i, i);

            // Apply the modified palette to the bitmap.
            bmp.Palette = palette;
            return palette;
        }

        public static ColorPalette FillIndexedPalette(
            Bitmap bmp, Color[] entries, int firstEntryIndex, bool makeOpaque)
        {
            // Get a COPY of the color palette used in the bitmap.
            ColorPalette palette = bmp.Palette;

            // Set the color entries in the bitmap palette.
            int numEntriesToCopy = Math.Min(
                palette.Entries.Length - firstEntryIndex, entries.Length);
            for (int i = 0; i < numEntriesToCopy; i++)
            {
                Color c = entries[i];
                palette.Entries[firstEntryIndex + i] = makeOpaque ?
                    Color.FromArgb(255, c) : c;
            }

            // Apply the modified palette to the bitmap.
            bmp.Palette = palette;
            return palette;
        }

        /// <summary>
        /// Converts raw pixel data to <code>System.Drawing.Color</code>.
        /// </summary>
        /// <remarks>
        /// The pixel data must be stored in an integral number of bytes. The
        /// bit-fields A, R, G, B are stored consecutively from the LSB to the
        /// MSB, right-aligned at the LSB. 
        ///
        /// Each primary (i.e. non-alpha) channel is assumed to take
        /// MIN(bpp/3, 8) bits. (See Page 10 of TGA 2.0 spec.) In other words,
        /// if bpp >= 24, 8 bits are used for each primary channel, and 
        /// (bpp - 24) bits are used by the alpha channel. If bpp is less than
        /// 24, (bpp/3) bits are used for each primary channel, and the extra
        /// bits are used by the alpha channel.
        ///
        /// Typical bpp values are 15, 16, 24 and 32.
        /// </remarks>
        public static Color ConvertPixelToColor(byte[] data, int startIndex, int bpp)
        {
            int bytesPerPixel = (bpp + 7) / 8;

            // Read bytesPerPixel bytes into a 32-bit integer.
            uint x = 0;
            for (int j = 0; j < bytesPerPixel; j++)
                x |= (uint)data[startIndex + j] << (j * 8);

            // Decode A,R,G,B in reverse order.
            int bitsPerColor = Math.Min(bpp / 3, 8);
            int bitsPerAlpha = bpp - 3 * bitsPerColor;
            int colorMask = (1 << bitsPerColor) - 1;
            int alphaMask = (1 << bitsPerAlpha) - 1;
            byte b = (byte)((x >> (0 * bitsPerColor)) & colorMask);
            byte g = (byte)((x >> (1 * bitsPerColor)) & colorMask);
            byte r = (byte)((x >> (2 * bitsPerColor)) & colorMask);
            byte a = (byte)((x >> (3 * bitsPerColor)) & alphaMask);

            // Left-align the component values to MSB.
            b <<= 8 - bitsPerColor;
            g <<= 8 - bitsPerColor;
            r <<= 8 - bitsPerColor;
            a <<= 8 - bitsPerAlpha;

            // Return the color.
            return Color.FromArgb(bitsPerAlpha == 0 ? 255 : a, r, g, b);
        }
    }

    #region Serialization

    internal class TgaBinaryReader : BinaryReader
    {
        public TgaBinaryReader(Stream stream)
            : base(stream) { }

        public void ReadFull(byte[] buffer, int offset, int count)
        {
            while (count > 0)
            {
                int actual = Read(buffer, offset, count);
                if (actual == 0)
                    throw new EndOfStreamException();
                offset += actual;
                count -= offset;
            }
        }

        /// <summary>
        /// Reads the given number of bytes and convert it to a string using
        /// UTF-8 encoding, up to the first NULL byte if any.
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="numberOfBytes"></param>
        /// <returns></returns>
        public string ReadFixedString(int numberOfBytes)
        {
            byte[] buffer = new byte[numberOfBytes];
            ReadFull(buffer, 0, buffer.Length);

            int n = Array.IndexOf(buffer, (byte)0);
            if (n < 0)
                n = buffer.Length;

            return Encoding.UTF8.GetString(buffer, 0, n);
        }

        public TgaFileHeader ReadFileHeader()
        {
            TgaFileHeader x = new TgaFileHeader();
            x.ImageIdLength = ReadByte();
            x.PaletteType = (TgaPaletteType)ReadByte();
            x.ImageType = (TgaImageType)ReadByte();
            x.PaletteStartIndex = ReadUInt16();
            x.PaletteEntryCount = ReadUInt16();
            x.PaletteColorDepth = ReadByte();
            x.ImageOriginX = ReadUInt16();
            x.ImageOriginY = ReadUInt16();
            x.ImageWidth = ReadUInt16();
            x.ImageHeight = ReadUInt16();
            x.PixelDepth = ReadByte();
            x.ImageDescriptor = ReadByte();
            // Image ID - technically not part of File Header and not 
            // necessarily a string, but included here for convenience.
            x.ImageId = ReadFixedString(x.ImageIdLength);
            return x;
        }

        public TgaFileFooter ReadFileFooter()
        {
            TgaFileFooter x = new TgaFileFooter();
            x.ExtensionAreaOffset = ReadUInt32();
            x.DeveloperAreaOffset = ReadUInt32();
            x.Signature = ReadFixedString(18);
            return x;
        }

        public byte[] ReadImageId(byte imageIdLength)
        {
            byte[] imageId = new byte[imageIdLength];
            ReadFull(imageId, 0, imageId.Length);
            return imageId;
        }

        public TgaDeveloperArea ReadDeveloperArea()
        {
            TgaDeveloperArea x = new TgaDeveloperArea();
            x.EntryCount = ReadUInt16();
            x.Entries = new TgaDeveloperEntry[x.EntryCount];
            for (int i = 0; i < x.EntryCount; i++)
            {
                TgaDeveloperEntry e = new TgaDeveloperEntry();
                e.Tag = ReadUInt16();
                e.Offset = ReadUInt32();
                e.Size = ReadUInt32();
                x.Entries[i] = e;
            }
            return x;
        }

        public TgaExtensionArea ReadExtensionArea()
        {
            TgaExtensionArea x = new TgaExtensionArea();
            x.ExtensionSize = ReadUInt16();
            x.AuthorName = ReadFixedString(41);
            x.AuthorComments = new string[4];
            x.AuthorComments[0] = ReadFixedString(81);
            x.AuthorComments[1] = ReadFixedString(81);
            x.AuthorComments[2] = ReadFixedString(81);
            x.AuthorComments[3] = ReadFixedString(81);
            x.Timestamp = ReadDateTime();
            x.JobName = ReadFixedString(41);
            x.JobDuration = ReadDuration();
            x.SoftwareName = ReadFixedString(41);
            x.SoftwareVersion = ReadVersion();
            x.BackgroundColor = ReadColor();
            x.PixelAspectRatio = ReadRatio();
            x.GammaValue = ReadRatio();
            x.ColorCorrectionTableOffset = ReadUInt32();
            x.ThumbnailOffset = ReadUInt32();
            x.ScanLineTableOffset = ReadUInt32();
            return x;
        }

        public DateTime ReadDateTime()
        {
            short month = ReadInt16();
            short day = ReadInt16();
            short year = ReadInt16();
            short hour = ReadInt16();
            short minute = ReadInt16();
            short second = ReadInt16();
            if (year == 0)
                return DateTime.MinValue;
            try
            {
                return new DateTime(year, month, day, hour, minute, second);
            }
            catch (ArgumentOutOfRangeException)
            {
                return DateTime.MinValue;
            }
        }

        public TimeSpan ReadDuration()
        {
            ushort hours = ReadUInt16();
            ushort minutes = ReadUInt16();
            ushort seconds = ReadUInt16();
            return new TimeSpan(hours, minutes, seconds);
        }

        public TgaVersion ReadVersion()
        {
            TgaVersion x = new TgaVersion();
            x.Number = ReadUInt16();
            x.Letter = ReadByte();
            return x;
        }

        public Color ReadColor()
        {
            return Color.FromArgb(ReadInt32());
        }

        public TgaRatio ReadRatio()
        {
            TgaRatio x = new TgaRatio();
            x.Numerator = ReadUInt16();
            x.Denominator = ReadUInt16();
            return x;
        }

        public Color[] ReadPalette(int bpp, int entryCount)
        {
            // Read raw palette entry data.
            int bytesPerEntry = (bpp + 7) / 8;
            byte[] data = new byte[bytesPerEntry * entryCount];
            ReadFull(data, 0, data.Length);

            // Convert pixel to color.
            Color[] pal = new Color[entryCount];
            for (int i = 0; i < entryCount; i++)
            {
                pal[i] = TgaBitmapHelper.ConvertPixelToColor(data, i * bytesPerEntry, bpp);
            }
            return pal;
        }

        /// <summary>
        /// Reads TGA pixel data scanline by scanline from a stream. 
        /// The pixel data may be uncompressed or run-length encoded.
        /// </summary>
        /// <param name="stream">The stream to read data from.</param>
        /// <param name="compressed">Whether the pixel data is run-length
        /// encoded.</param>
        /// <param name="bpp">Number of bits per pixel.</param>
        /// <param name="width">Width of the bitmap, in pixels.</param>
        /// <param name="height">Height of the bitmap, in pixels.</param>
        /// <returns>
        /// An array of scanlines where each scanline is an array of bytes 
        /// containing the uncompressed raw pixel data. The scanlines are
        /// NOT padded to align on a 4-byte boundary.
        /// </returns>
        /// <remarks>
        /// This method reads pixel data as stored in the stream, ignoring
        /// the horizontal direction and vertical direction specified in the
        /// TGA header. The caller must adjust these directions later.
        /// </remarks>
        public byte[][] ReadPixelData(bool compressed, int bpp, int width, int height)
        {
            byte[][] scanlines = new byte[height][];

            // Return an empty array if the image is empty.
            if (bpp == 0 || width == 0 || height == 0)
                return new byte[height][];

            // Create a pixel stream based on whether the data is compressed.
            TgaPixelReader pixelReader = compressed ?
                new TgaRunLengthEncodedPixelReader(this, bpp) :
                new TgaPixelReader(this, bpp);

            // Read each scanline in turn. This helps to reduce the impact of
            // DoS attack in case (Width * Height) is too large.
            int bytesPerPixel = (bpp + 7) / 8;
            for (int y = 0; y < height; y++)
            {
                scanlines[y] = new byte[width * bytesPerPixel];
                pixelReader.ReadPixels(scanlines[y], 0, width);
            }
            return scanlines;
        }

        public TgaThumbnail ReadThumbnail(int bpp)
        {
            TgaThumbnail x = new TgaThumbnail();

            // The first two bytes are the width and height of the thumbnail.
            x.Width = ReadByte();
            x.Height = ReadByte();

            // Load the thumbnail pixel data, which is always uncompressed.
            x.PixelData = ReadPixelData(false, bpp, x.Width, x.Height);
            return x;
        }

        public uint[] ReadScanlineTable(ushort numScanlines)
        {
#if true
            uint[] scanlineOffsets = new uint[numScanlines];
            for (int i = 0; i < numScanlines; i++)
                scanlineOffsets[i] = ReadUInt32();
            return scanlineOffsets;
#else
            // The following code is faster, but it doesn't handle endianness.
            byte[] tmp = new byte[4 * height];
            if (!reader.BaseStream.TryReadFull(tmp, 0, tmp.Length))
                throw new InvalidDataException("Cannot read scan line table.");
            Buffer.BlockCopy(tmp, 0, this.ExtensionArea.ScanLineTable, 0, tmp.Length);
#endif
        }

        public TgaColorCorrectionEntry[] ReadColorCorrectionTable()
        {
#if true
            TgaColorCorrectionEntry[] cct = new TgaColorCorrectionEntry[256];
            for (int i = 0; i < 256; i++)
            {
                cct[i].Blue = ReadUInt16();
                cct[i].Green = ReadUInt16();
                cct[i].Red = ReadUInt16();
                cct[i].Alpha = ReadUInt16();
            }
            return cct;
#else
            // The following code is faster, but it doesn't handle endianness.
            byte[] tmp = new byte[256 * 4 * 2];
            if (!reader.BaseStream.TryReadFull(tmp, 0, tmp.Length))
                throw new InvalidDataException("Cannot read color correction table.");
            Buffer.BlockCopy(tmp, 0, ExtensionArea.ColorCorrectionTable, 0, tmp.Length);
#endif
        }
    }

    internal class TgaPixelReader
    {
        protected TgaBinaryReader reader;
        protected int bytesPerPixel;

        public TgaPixelReader(TgaBinaryReader reader, int bpp)
        {
            this.reader = reader;
            this.bytesPerPixel = (bpp + 7) / 8;
        }

        public virtual void ReadPixels(byte[] buffer, int startIndex, int numPixels)
        {
            reader.ReadFull(buffer, startIndex, bytesPerPixel * numPixels);
        }
    }

    internal class TgaRunLengthEncodedPixelReader : TgaPixelReader
    {
        private byte[] buffer;
        private int bufferStart; // start index of buffered pixels
        private int bufferSize;  // number of pixels in buffer

        public TgaRunLengthEncodedPixelReader(TgaBinaryReader reader, int bpp)
            : base(reader, bpp)
        {
            // Create a buffer of 128 pixels, which is the maximum number of
            // pixels that can be encoded in a single packet.
            buffer = new byte[bytesPerPixel * 128];
            bufferStart = 0;
            bufferSize = 0;
        }

        public override void ReadPixels(byte[] data, int startIndex, int numPixels)
        {
            while (numPixels > 0)
            {
                if (bufferSize == 0)
                    FillBuffer();
                int n = Math.Min(numPixels, bufferSize);
                Buffer.BlockCopy(buffer, bufferStart, data, startIndex, n * bytesPerPixel);
                bufferSize -= n;
                bufferStart += n * bytesPerPixel;
                numPixels -= n;
                startIndex += n * bytesPerPixel;
            }
        }

        private void FillBuffer()
        {
            // Read RLE packet header byte. The highest bit of this byte
            // indicates whether the packet is run-length encoded (1) or
            // raw (0). The lower 7 bits indicate the number of pixels
            // contained in this packet, minus 1.
            int packetHeader = reader.ReadByte();
            int pixelCount = (packetHeader & 0x7F) + 1;

            if ((packetHeader & 0x80) != 0) // RLE packet
            {
                // Read one pixel, and duplicate it pixelCount times.
                reader.ReadFull(buffer, 0, bytesPerPixel);
                for (int i = 1; i < pixelCount; i++)
                {
                    Buffer.BlockCopy(buffer, 0, buffer, i * bytesPerPixel, bytesPerPixel);
                }
                bufferStart = 0;
                bufferSize = pixelCount;
            }
            else // raw packet
            {
                reader.ReadFull(buffer, 0, pixelCount * bytesPerPixel);
                bufferStart = 0;
                bufferSize = pixelCount;
            }
        }
    }

    #endregion
}
